iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0

昨天介紹了使用 modulepreload 預先加載 module worker,可以較早取得重要的資源,用意跟 preload 是一樣的,但不知道有沒有人覺得好奇,既然兩者的用意一樣,那為什麼不直接沿用 <link rel="preload" /> 的寫法就好,而是要多創一個 <link rel="modulepreload" /> 呢? 這也是這篇文章 Preloading modules 所提到的想法,提到這部分想法的文章總共分三段,其中比較重點的是前面兩段,以下我就節錄這兩段的部分並介紹背後的故事:

第一段

OK, so why doesn't work for modules?

This is where things get tricky. There are several credentials modes for resources, and in order to get a cache hit they must match, otherwise you end up fetching the resource twice. Needless to say, double-fetching is bad, because it wastes the user's bandwidth and makes them wait longer, for no good reason.

這件事本身是有點棘手的,對於資源來說會有數種 credential mode 的定義 ,為了能夠覆用 cache,對於這些資源來說,他們的 credential mode 也是必須一致的,如果這些資源的 credential mode 不一致,最終你等於載入相同的資源兩次,而這件事很明顯的就是不好的,因為使用者會消耗多餘的網路頻寬、使網頁載入的時間更長,等得更久。

解釋

這裡我們先釐清一下什麼是 資源資源 轉換成程式上的表達,可以想像成就是 <srcipt src="..." /> 這種寫法,代表需要花時間載入的外部 script

credential mode 是牽涉到 CORS 的一個專有名詞 (建議各位先跳到後面的 補充小知識 1 - CORS 概念複習 閱讀,有助於瞭解接下來的部分),第一段的結論大致上可以用以下程式碼來表示:

<link
  rel="preload"
  src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"
  as="script"
/>
<script 
  src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js" 
  crossorigin="use-credentials"
/>

這裡的 <link>, <script> 指向的是同一個資源,但一個沒有加 crossorigin、一個有加 crossorigin,導致這兩個寫法的 credential mode 是不同的 (same-origin vs include),如果是這種狀況的話,就可能導致 preload 的使用沒有意義,因為 credential mode 不同,快取不會命中,資源會重複下載兩遍

我想一般提到的 HTTP cache 是關於 Cache-Controlmax-ageExpires 這類的設定等,不太有提到 preload 資源script 資源 之間的快取關係,我有找到一篇文章 使用 CORS 與 cache 時的注意事項 中提到對於同一個資源的多次請求,如果第一次是 no-cors 請求資源,資源在第一次請求後會被快取起來,但如果第二次對同樣的資源發起 cors 請求,會直接拿第一次請求的快取來用,但因為第一次請求是 no-cors,所以快取回來的 response header 沒有 Access-Control-Allow-Origin,而第二次的 cors 請求發現快取的 response header 中沒有 Access-Control-Allow-Origin 時就會違反 CORS 規範而直接請求失敗。但這篇文章探討的是對於同樣資源的兩次請求狀況,與這第一段探討 preloadscript 的快取關聯,應該是不太一樣的

其實對於這第一段我一直不太確定理解的對不對,但最後是看到這個 討論 提到 <link rel="preload" /><script /> 需要有同樣的 credential mode 才能命中快取,我想應該是這樣沒錯,如果有錯的話再幫忙提醒謝謝。

第二段

要進入第二段了,我覺得這段是最複雜的,找了很多資料後才搞懂在說什麼...

For and tags, you can set the credentials mode with the crossorigin attribute. However, it turns out that a with no crossorigin attribute indicates a credentials mode of omit, which doesn't exist for . This means that you would have to change the crossorigin attribute in both your and to one of the other values, and you might not have an easy way of doing so if what you're trying to preload is a dependency of other modules.

對於 <script><link> 標籤來說,你可以根據 crossorigin 設置 credentials mode。但是 <script type="module"> 這種不加 crossorigin 的寫法指明了 credentials modeOmit,而 Omit 這個值並不存在於 <link rel="preload">。這表示你必須同時的去更改 <script> 以及 <link> 標籤中的 crossorigin,但這並不是一件容易做到的事,當你想要 preload 的資源有可能是其他 module 之中的依賴項時

解釋

這段是我最看不懂的地方,其實目前在 MDN 中完全找不到 <script type="module">crossorigin 說明,最後是在規範中看到這兩點:

再搭配上 補充小知識 1-1. crossorigin,可以推斷 目前 <script type="module"> 在不加 crossorigin 時預設的 credentials modesame-origin,那麼為什麼這段直接說預設是 Omit 呢?原來是 <script type="module"> 的用法在 剛出來時 credentials mode 的預設值設成跟 fetch() 一樣,都是 Omit,是後來 經過一些討論後 才讓 <script type="module">fetch()credentials mode 預設值都變成 same-origin 的 (https://github.com/whatwg/fetch/pull/585 )

知道以上這段歷史淵源後終於可以看懂這第二段在講什麼了,<script type="module"> 一開始預設值是 Omit,而在 <link rel="preload"> 標籤中不論怎麼更改 crossorigin 的設定值也無法使其 credentials mode 等於 Omit,再搭配第一段的說明,這代表在以前一開始的時候 <script type="module"><link rel="preload"> 就是難以搭配使快取命中的,因為前者的 credentials mode 預設為 Omit,而後者卻沒辦法設定成 Omit

但發展到現在 <script type="module">credentials mode 預設值已經是 same-origin 了,所以事實上可以用 <link rel="preload" crossorigin> 的方式加載並快取 module script,但即使是這樣還是會 有些缺點產生,所以最終才誕生了 <link rel="modulepreload"> 的這個寫法

補充小知識

1. CORS 概念複習

因為這篇文章提到許多 CORS 的專有名詞,我覺得如果搞混這些名詞定義的話,很難看得懂這故事的前因後果,所以以下會先定義 CORS 相關的名詞:

1-1. crossorigin

<script 
  src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js" 
  crossorigin
/>

當要加載外部資源時,可以設置 crossorigin 屬性,這個屬性代表請求的外部資源是否採用 CORS 政策處理,crossorigin 的屬性有三種可能,而這三種又會影響到 request's moderequest's credential mode 的值

  • 沒有設置 crossorigin 時,也就是完全不寫 crossorigin 這個屬性時
    request's mode 等於 no-corsrequest's credential mode 等於 same-origin,這代表此時的請求完全沒有要用到 CORS 政策的意思
    P.S. 沒有設置 crossorigin 時,request's credential mode 的值我有點不太確定,MDN 上只有提到不寫 crossorigin 時,等同於不使用 CORS 政策。這篇文章 有寫到沒有設置 crossorigin 時,request's credential mode 的值為 include,但我怎麼看都覺得 spec 中的意思是指 No CORS 狀況下,request's credential mode 的值會設為 same-origin

  • 設置 crossorigin""(空字串)"anonymous" 或是其他的任意字串
    視作定義成 "anonymous",此時 request's mode 等於 corsrequest's credential mode 等於 same-origin

  • 設置 crossorigin"use-credentials"
    此時 request's mode 等於 corsrequest's credential mode 等於 include

crossorigin request's mode request's credential mode
無設置 no-cors same-origin
"anonymous" cors same-origin
"use-credentials" cors include

1-2. request's mode

request's mode 是進行 api 呼叫 (fetch, XMLHttpRequest) 時,所攜帶的一個參數值,主要用來代表這個請求是不是 CORS

fetch(url, {
  body: JSON.stringify(data),
  headers: {
    "user-agent": "Mozilla/4.0 MDN Example",
    "content-type": "application/json",
  },
  method: "POST",
  mode: "cors", // here
})

request's mode 總共有五個可能的值,same-originno-corscorsnavigatewebsocket,根據以上 crossorigin 設置的結果,我們目前只討論 no-corscors

  • no-cors
    簡單來說這代表的是發出的請求並不牽涉到 CORS 政策,但實際上根據你打的 api 是否同源或不同源可能會有令人意外的結果,請參照 把 fetch mode 設成 no-corsFetch 的使用注意事項

  • cors
    顧名思義,這代表著這個請求是跨域請求,所以一切的規則都要符合 CORS 政策

1-3. request's credential mode

request's credential mode 也是在呼叫 api 時,所攜帶的一個參數值,代表在進行跨域請求時,是否要送出或是接收 cookie 等這種憑證訊息

fetch(url, {
  body: JSON.stringify(data),
  headers: {
    "user-agent": "Mozilla/4.0 MDN Example",
    "content-type": "application/json",
  },
  method: "POST",
  credentials: "same-origin", // here
})

request's credential mode 有三種可能的值,omitsame-origininclude

  • omit
    在任何狀況下都不要發送、接收 cookie

  • same-origin
    預設值。在請求網域同源的狀況下可以發送、接收 cookie,但在不同源的狀況下則不發送、接收 cookie

  • include
    在任何狀況下都發送、接收 cookie,代表即使不同源的網域,也會將 cookie 資訊送出去

尚未解決的小疑問

  1. 在研究的過程中發現了一個叫做 create a potential-CORS request 的東西,看起來是在執行 fetch() 時,去決定 request's mode, request's credential mode 值的步驟,但這部分說實在的我還是沒看懂,如果剛好有了解的大大再請幫忙補充,謝謝~

Reference

跨源相关机制综述(三):crossorigin
研究的過程中卡關卡了很久,還好找到這篇文章完整提到過去的歷史淵源,幫助我能夠理解寫出這篇文章

ECMAScript modules in browsers
這篇提到很多關於 module script 在瀏覽器中的行為,也講到 module script 預設就是遵守 CORS


上一篇
使用 modulepreload 優先下載 module worker 檔案
下一篇
封裝 Web worker 的套件 - Comlink
系列文
網頁的另一個大腦:從基礎到進階掌握 Web Worker 技術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言